/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.j4sc.service;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Date;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.cookie.DateUtils;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

import com.googlecode.j4sc.UserCredentials;
import com.googlecode.j4sc.conversion.Conversion;
import com.googlecode.j4sc.service.SubmissionServiceException.ServiceError;
import com.googlecode.j4sc.submission.MultiPartMimeBodyBuilder;
import com.googlecode.j4sc.submission.Submission;
import com.googlecode.j4sc.submission.SubmissionReference;

/**
 * The Service is used to {@link #submit(Submission)} to the SpinVox Create
 * Service for processing and {@link #retrieve(SubmissionReference)} a
 * {@link Conversion} which contains the converted text.
 * <p>
 * The {@link #DEVELOPMENT} and {@link #LIVE} static instances are provided
 * for convenience for submitting and retrieving from the two version of the 
 * service. 
 * 
 * @author Raj Patel, Dariusz Lorenc
 */
public class Service {
	
	/**
	 * The SpinVox Create development service for sending free test requests
	 */
	public static final Service DEVELOPMENT = createQuietly("https://dev.api.spinvox.com");
	
	/**
	 * The SpinVox Create live service for submitting conversion requests
	 * (charged from your conversion credits)
	 */
	public static final Service LIVE = createQuietly("https://live.api.spinvox.com");

	private static final int CONNECTION_TIMEOUT = 10000;
	private static final int PORT = 443;
	private static final String REALM = "spinvoxapi";

	private final URI uri;
	private int responseTimeoutInMillis = 60000;

	private static Service createQuietly(String uri) {
		try {
			return new Service(new URI(uri));
		} catch (URISyntaxException e) {
			return null;
		}
	}
	
	/**
	 * Construct service instance for provided URI
	 * 
	 * @param uri
	 * @throws URISyntaxException
	 *             thrown if the URI is syntactically invalid
	 */
	public Service(URI uri) throws URISyntaxException {
		this.uri = uri;
	}
	
	/**
	 * Gets instance of URI of the service
	 * 
	 * @return instance of URI of the service
	 */
	public URI getUri() {
		return uri;
	}

	/**
	 * Sets the response timeout for the service, default is 1 min (60000 ms)
	 * 
	 * @param timeoutInMillis
	 *            the timeout in milliseconds
	 */
	public void setResponseTimeout(int timeoutInMillis) {
		this.responseTimeoutInMillis = timeoutInMillis;
	}
	
	/**
	 * Submit a {@link Submission} to the service for conversion
	 * 
	 * @param submission
	 *            the submission
	 * @return the {@link SubmissionReference} to be used when retrieving the
	 *         conversion later using the {@link #retrieve(SubmissionReference)}
	 *         method
	 * @throws ServiceException
	 *             thrown if the service responds with an error
	 * @throws IOException
	 *             thrown on I/O errors
	 */
	public SubmissionReference submit(Submission submission) throws ServiceException, IOException {
		HttpPost httppost = new HttpPost(uri);
		setHeaders(httppost);
		httppost.setEntity(new CustomStringEntity(submission.createMimeBody(), "multipart/mixed; " +
				"boundary=\"" + MultiPartMimeBodyBuilder.MIME_BOUNDARY + "\""));

		HttpResponse response = executeHttpMethod(httppost, submission.getUserCredentials());
		
		SubmissionResult result = new SubmissionResult(response);
		
		return new SubmissionReference(result.getLocationUri(), submission.getUserCredentials());
	}
	
	/**
	 * Retrieve a {@link Conversion} from this Service using the
	 * {@link SubmissionReference}
	 * 
	 * @param reference
	 *            the {@link SubmissionReference} as returned from
	 *            {@link Service#submit(Submission)} method
	 * @return the Conversion
	 * @throws ServiceException
	 *             thrown if the service responds with an error
	 * @throws IOException
	 *             thrown on I/O errors
	 */
	public Conversion retrieve(SubmissionReference reference) throws ServiceException, IOException {
		HttpGet httpget = new HttpGet(reference.getUri());
		HttpResponse response = executeHttpMethod(httpget, reference.getUserCredentials());
	
		return new RetrieveResult(response).getConversion();
	}
	
	private HttpResponse executeHttpMethod(HttpRequestBase httpRequest, UserCredentials userCredentials) throws IOException {
		DefaultHttpClient httpclient = new DefaultHttpClient();
		
		if (userCredentials != null) { 
			configureAuthentication(userCredentials, httpclient);
		}
		setTimeouts(httpclient);
		
		return httpclient.execute(httpRequest);
	}
	
	private void setTimeouts(DefaultHttpClient httpclient) {
		HttpParams params = httpclient.getParams();
		HttpConnectionParams.setSoTimeout(params, responseTimeoutInMillis);
		HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
	}

	private void configureAuthentication(UserCredentials userCredentials, DefaultHttpClient httpclient) {
		Credentials defaultcreds = new UsernamePasswordCredentials(
				userCredentials.getUsername(), userCredentials.getPassword());
        httpclient.getCredentialsProvider().setCredentials(new AuthScope(uri.getHost(), PORT, REALM), defaultcreds);
	}

	private void setHeaders(HttpPost httppost) {
		httppost.setHeader("Connection", "keep-alive");
        httppost.setHeader("User-Agent", "j4sc");
        httppost.setHeader("Pragma", "no-cache");
        httppost.setHeader("Cache-control", "no-cache");
        httppost.setHeader("MIME-version", "1.0");
        httppost.setHeader("Date", DateUtils.formatDate(new Date()));
	}
	
	private static class CustomStringEntity extends StringEntity {

        public CustomStringEntity(String content, String contentType) throws UnsupportedEncodingException {
            super(content);
            super.setContentType(new BasicHeader("Content-Type", contentType));
        }
    }
	
	private static class SubmissionResult {

		private URI locationUri;
		
		private enum ResponseHeader {
			ERROR("X-Error"),
			LOCATION("Location");
			
			private final String headerName;

			private ResponseHeader(String headerName) {
				this.headerName = headerName;
			}
			
			private String getHeaderName() {
				return headerName;
			}
		}

		public SubmissionResult(HttpResponse response) throws ServiceException {
			int statusCode = response.getStatusLine().getStatusCode();
			
			if(statusCode == HttpStatus.SC_ACCEPTED) {
				locationUri = extractLocationUri(response);
			} else {
				throw new SubmissionServiceException(extractServiceError(response));
			}
		}
		
		private URI getLocationUri() {
			return locationUri;
		}
		
		private URI extractLocationUri(HttpResponse response) throws ServiceException {
			try {
				return new URI(extractSingleHeaderValue(ResponseHeader.LOCATION, response));
			} catch (URISyntaxException e) {
				throw new ServiceException(response.getStatusLine().getStatusCode(), 
						String.format("Service returned invalid location URI - %s", e.getMessage()));
			}
		}
		
		private ServiceError extractServiceError(HttpResponse response) throws ServiceException {
			String errorValue = extractSingleHeaderValue(ResponseHeader.ERROR, response);
			// remove hyphens to convert error value to enum constant
			return ServiceError.valueOf(errorValue.replace("-", StringUtils.EMPTY));
		}

		private String extractSingleHeaderValue(ResponseHeader header, HttpResponse response) throws ServiceException {
			Header[] errorHeaders = response.getHeaders(header.getHeaderName());
			if (ArrayUtils.isEmpty(errorHeaders)) {
				throw new ServiceException(response.getStatusLine().getStatusCode(), 
						String.format("Service response is missing [%s] header", header.headerName));
			}
			return errorHeaders[0].getValue().trim();
		}
	}	
	
	private static class RetrieveResult {

		private Conversion conversion;

		public RetrieveResult(HttpResponse response) throws IOException, ServiceException {
			int statusCode = response.getStatusLine().getStatusCode();
			String reasonPhrase = response.getStatusLine().getReasonPhrase();
			String xml = IOUtils.toString(response.getEntity().getContent());
			conversion = new Conversion(statusCode, reasonPhrase, xml);
		}
		
		private Conversion getConversion() {
			return conversion;
		}
	}
}